{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7832bb1a",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| default_exp jupyter"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "257b6e4b",
   "metadata": {},
   "source": [
    "# Jupyter compatibility\n",
    "\n",
    "> Use FastHTML in Jupyter notebooks\n",
    "- skip_exec: true"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2c69d9d0",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "import asyncio, socket, time, uvicorn\n",
    "from threading import Thread\n",
    "from fastcore.utils import *\n",
    "from fastcore.meta import delegates\n",
    "from fasthtml.common import *\n",
    "from fasthtml.common import show as _show\n",
    "from fastcore.parallel import startthread\n",
    "try: from IPython.display import HTML,Markdown,display\n",
    "except ImportError: pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a1c7b207",
   "metadata": {},
   "outputs": [],
   "source": [
    "from httpx import get, AsyncClient"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fab2984f",
   "metadata": {},
   "source": [
    "## Helper functions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a5d3a8f7",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def nb_serve(app, log_level=\"error\", port=8000, host='0.0.0.0', **kwargs):\n",
    "    \"Start a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`\"\n",
    "    server = uvicorn.Server(uvicorn.Config(app, log_level=log_level, host=host, port=port, **kwargs))\n",
    "    async def async_run_server(server): await server.serve()\n",
    "    @startthread\n",
    "    def run_server(): asyncio.run(async_run_server(server))\n",
    "    while not server.started: time.sleep(0.01)\n",
    "    return server"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3242080c",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "async def nb_serve_async(app, log_level=\"error\", port=8000, host='0.0.0.0', **kwargs):\n",
    "    \"Async version of `nb_serve`\"\n",
    "    server = uvicorn.Server(uvicorn.Config(app, log_level=log_level, host=host, port=port, **kwargs))\n",
    "    asyncio.get_running_loop().create_task(server.serve())\n",
    "    while not server.started: await asyncio.sleep(0.01)\n",
    "    return server"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "508917bc",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def is_port_free(port, host='localhost'):\n",
    "    \"Check if `port` is free on `host`\"\n",
    "    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
    "    try:\n",
    "        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n",
    "        sock.bind((host, port))\n",
    "        return True\n",
    "    except OSError: return False\n",
    "    finally: sock.close()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1779cb76",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def wait_port_free(port, host='localhost', max_wait=3):\n",
    "    \"Wait for `port` to be free on `host`\"\n",
    "    start_time = time.time()\n",
    "    while not is_port_free(port):\n",
    "        if time.time() - start_time>max_wait: return print(f\"Timeout\")\n",
    "        time.sleep(0.1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dcb6ada4",
   "metadata": {},
   "source": [
    "## Using FastHTML in Jupyter"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "654b36bb",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@delegates(_show)\n",
    "def show(*s, **kwargs):\n",
    "    \"Same as fasthtml.components.show, but also adds `htmx.process()`\"\n",
    "    if IN_NOTEBOOK: return _show(*s, Script('if (window.htmx) htmx.process(document.body)'), **kwargs)\n",
    "    return _show(*s, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6a962b54",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def render_ft(**kw):\n",
    "    \"Call once in a notebook or solveit dialog to auto-render components\"\n",
    "    @patch\n",
    "    def _repr_markdown_(self:FT):\n",
    "        scr_proc = Script('if (window.htmx) htmx.process(document.body)')\n",
    "        return to_xml(Div(self, scr_proc, **kw))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1daaa0e1",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def htmx_config_port(port=8000):\n",
    "    display(HTML('''\n",
    "<script>\n",
    "document.body.addEventListener('htmx:configRequest', (event) => {\n",
    "    if(event.detail.path.includes('://')) return;\n",
    "    htmx.config.selfRequestsOnly=false;\n",
    "    event.detail.path = `${location.protocol}//${location.hostname}:%s${event.detail.path}`;\n",
    "});\n",
    "</script>''' % port))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "29a834a5",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class JupyUvi:\n",
    "    \"Start and stop a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`\"\n",
    "    def __init__(self, app, log_level=\"error\", host='0.0.0.0', port=8000, start=True, **kwargs):\n",
    "        self.kwargs = kwargs\n",
    "        store_attr(but='start')\n",
    "        self.server = None\n",
    "        if start: self.start()\n",
    "        if not os.environ.get('IN_SOLVEIT'): htmx_config_port(port)\n",
    "\n",
    "    def start(self):\n",
    "        self.server = nb_serve(self.app, log_level=self.log_level, host=self.host, port=self.port, **self.kwargs)\n",
    "\n",
    "    async def start_async(self):\n",
    "        self.server = await nb_serve_async(self.app, log_level=self.log_level, host=self.host, port=self.port, **self.kwargs)\n",
    "\n",
    "    def stop(self):\n",
    "        self.server.should_exit = True\n",
    "        wait_port_free(self.port)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7c84a5f2",
   "metadata": {},
   "source": [
    "Creating an object of this class also starts the Uvicorn server. It runs in a separate thread, so you can use normal HTTP client functions in a notebook. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0f4b31e9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<script>\n",
       "document.body.addEventListener('htmx:configRequest', (event) => {\n",
       "    if(event.detail.path.includes('://')) return;\n",
       "    htmx.config.selfRequestsOnly=false;\n",
       "    event.detail.path = `${location.protocol}//${location.hostname}:8000${event.detail.path}`;\n",
       "});\n",
       "</script>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "app = FastHTML()\n",
    "rt = app.route\n",
    "\n",
    "@app.route\n",
    "def index(): return 'hi'\n",
    "\n",
    "port = 8000\n",
    "server = JupyUvi(app, port=port)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9e57a6a1",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'hi'"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "get(f'http://localhost:{port}').text"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "addaa9c3",
   "metadata": {},
   "source": [
    "You can stop the server, modify routes, and start the server again without restarting the notebook or recreating the server or application."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8ea9128a",
   "metadata": {},
   "outputs": [],
   "source": [
    "server.stop()\n",
    "await asyncio.sleep(0.2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "97bfb966",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<script>\n",
       "document.body.addEventListener('htmx:configRequest', (event) => {\n",
       "    if(event.detail.path.includes('://')) return;\n",
       "    htmx.config.selfRequestsOnly=false;\n",
       "    event.detail.path = `${location.protocol}//${location.hostname}:8000${event.detail.path}`;\n",
       "});\n",
       "</script>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "app = FastHTML()\n",
    "rt = app.route\n",
    "\n",
    "@app.route\n",
    "async def index(): return 'hi'\n",
    "\n",
    "server = JupyUvi(app, port=port, start=False)\n",
    "await server.start_async()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8945d436",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "hi\n"
     ]
    }
   ],
   "source": [
    "print((await AsyncClient().get(f'http://localhost:{port}')).text)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c881cfc5",
   "metadata": {},
   "outputs": [],
   "source": [
    "server.stop()\n",
    "await asyncio.sleep(0.2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9134035e",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class JupyUviAsync(JupyUvi):\n",
    "    \"Start and stop an async Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`\"\n",
    "    def __init__(self, app, log_level=\"error\", host='0.0.0.0', port=8000, **kwargs):\n",
    "        super().__init__(app, log_level=log_level, host=host, port=port, start=False, **kwargs)\n",
    "\n",
    "    async def start(self):\n",
    "        self.server = await nb_serve_async(self.app, log_level=self.log_level, host=self.host, port=self.port, **self.kwargs)\n",
    "\n",
    "    def stop(self):\n",
    "        self.server.should_exit = True\n",
    "        wait_port_free(self.port)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "959eb254",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<script>\n",
       "document.body.addEventListener('htmx:configRequest', (event) => {\n",
       "    if(event.detail.path.includes('://')) return;\n",
       "    htmx.config.selfRequestsOnly=false;\n",
       "    event.detail.path = `${location.protocol}//${location.hostname}:8000${event.detail.path}`;\n",
       "});\n",
       "</script>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "server = JupyUviAsync(app, port=port)\n",
    "await server.start()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e4550d23",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "hi\n"
     ]
    }
   ],
   "source": [
    "async with AsyncClient() as client:\n",
    "    r = await client.get(f'http://localhost:{port}')\n",
    "print(r.text)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7869af78",
   "metadata": {},
   "outputs": [],
   "source": [
    "server.stop()\n",
    "await asyncio.sleep(0.2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f3db86f5",
   "metadata": {},
   "source": [
    "### Using a notebook as a web app"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12b03a53",
   "metadata": {},
   "source": [
    "You can also run an HTMX web app directly in a notebook. To make this work, you have to add the default FastHTML headers to the DOM of the notebook with `show(*def_hdrs())`. Additionally, you might find it convenient to use *auto_id* mode, in which the ID of an `FT` object is automatically generated if not provided."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9c2a85b6",
   "metadata": {},
   "outputs": [],
   "source": [
    "fh_cfg['auto_id' ]=True"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4244901a",
   "metadata": {},
   "source": [
    "After importing `fasthtml.jupyter` and calling `render_ft()`, FT components render directly in the notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "694cdab5",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<meta charset=\"utf-8\">\n",
       "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n",
       "<script src=\"https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script><script id=\"_AxFC49bqTX_vvHITdKX34w\">if (window.htmx) htmx.process(document.body)</script>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "show(*def_hdrs())\n",
    "render_ft()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8dd94a9a",
   "metadata": {},
   "source": [
    "Handlers are written just like a regular web app:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "78d40711",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<script>\n",
       "document.body.addEventListener('htmx:configRequest', (event) => {\n",
       "    if(event.detail.path.includes('://')) return;\n",
       "    htmx.config.selfRequestsOnly=false;\n",
       "    event.detail.path = `${location.protocol}//${location.hostname}:8000${event.detail.path}`;\n",
       "});\n",
       "</script>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "server = JupyUvi(app, port=port)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a4f1b1f5",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "<div id=\"_PxvQ15IuRXm9FIIqzd0s2g\">\n",
       "  <div id=\"_BxxYJqh2RdOaxF3FIK0VBA\"></div>\n",
       "<script id=\"_T5tPsHW3S4CHyiz8qnhcTA\">if (window.htmx) htmx.process(document.body)</script></div>\n"
      ],
      "text/plain": [
       "div(('',),{'id': '_BxxYJqh2RdOaxF3FIK0VBA'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "(c := Div(''))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "17b96ef1",
   "metadata": {},
   "outputs": [],
   "source": [
    "@rt\n",
    "def hoho(): return P('loaded!'), Div('hee hee', id=c, hx_swap_oob='true')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b2c9a7ba",
   "metadata": {},
   "source": [
    "All the usual `hx_*` attributes can be used:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a694a095",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "<div id=\"_XgMVy0kiSoqMdapiYOvKjQ\">\n",
       "  <p hx-get=\"/hoho\" hx-trigger=\"load\" id=\"_x4TFzZPvTtWff74RmcHGVQ\">not loaded</p>\n",
       "<script id=\"_bjgxEv1nSTSRImWdUhYk4Q\">if (window.htmx) htmx.process(document.body)</script></div>\n"
      ],
      "text/plain": [
       "p(('not loaded',),{'hx-get': <fasthtml.core._mk_locfunc.<locals>._lf object>, 'hx-trigger': 'load', 'id': '_x4TFzZPvTtWff74RmcHGVQ'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "P('not loaded', hx_get=hoho, hx_trigger='load')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6addc4a3",
   "metadata": {},
   "source": [
    "FT components can be used directly both as `id` values and as `hx_target` values."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d1116727",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "<div id=\"_tLh3mf0ZRdecNW_hYz9eYA\">\n",
       "  <div id=\"_AsIIqL_PTs_nPASliaNYGA\"></div>\n",
       "<script id=\"_zVvvP66UT_aabITIdgho2g\">if (window.htmx) htmx.process(document.body)</script></div>\n"
      ],
      "text/plain": [
       "div(('',),{'id': '_AsIIqL_PTs_nPASliaNYGA'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "(c := Div(''))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9a79b3e5",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "<div id=\"_ofCzAKDvSsWqGb_i-9X95A\">\n",
       "  <p hx-get=\"/foo\" hx-trigger=\"load\" hx-target=\"#_AsIIqL_PTs_nPASliaNYGA\" id=\"_LjH2GVPJS_6wMMIQSQpyJA\">hi</p>\n",
       "<script id=\"_tWo0Xu7TSQGyuYfd68xuaA\">if (window.htmx) htmx.process(document.body)</script></div>\n"
      ],
      "text/plain": [
       "p(('hi',),{'hx-get': <fasthtml.core._mk_locfunc.<locals>._lf object>, 'hx-trigger': 'load', 'hx-target': '#_AsIIqL_PTs_nPASliaNYGA', 'id': '_LjH2GVPJS_6wMMIQSQpyJA'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "@rt\n",
    "def foo(): return Div('foo bar')\n",
    "P('hi', hx_get=foo, hx_trigger='load', hx_target=c)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c5b33bfd",
   "metadata": {},
   "outputs": [],
   "source": [
    "server.stop()\n",
    "await asyncio.sleep(0.2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2ab03e6f",
   "metadata": {},
   "source": [
    "### Running apps in an IFrame"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e3a55b5a",
   "metadata": {},
   "source": [
    "Using an IFrame can be a good idea to get complete isolation of the styles and scripts in an app. The `HTMX` function creates an auto-sizing IFrame for a web app."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a448e420",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "from starlette.testclient import TestClient\n",
    "from html import escape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "89b8984b",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def HTMX(path=\"/\", host='localhost', app=None, port=8000, height=\"auto\", link=False, iframe=True):\n",
    "    \"An iframe which displays the HTMX application in a notebook.\"\n",
    "    if isinstance(height, int): height = f\"{height}px\"\n",
    "    scr = \"\"\"{\n",
    "        let frame = this;\n",
    "        window.addEventListener('message', function(e) {\n",
    "            if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe\n",
    "            if (e.data.height) frame.style.height = (e.data.height+1) + 'px';\n",
    "        }, false);\n",
    "    }\"\"\" if height == \"auto\" else \"\"\n",
    "    proto = 'http' if host=='localhost' else 'https'\n",
    "    fullpath = f\"{proto}://{host}:{port}{path}\" if host else path\n",
    "    src = f'src=\"{fullpath}\"'\n",
    "    if link: display(HTML(f'<a href=\"{fullpath}\" target=\"_blank\">Open in new tab</a>'))\n",
    "    if isinstance(path, (FT,tuple,Safe)):\n",
    "        assert app, 'Need an app to render a component'\n",
    "        route = f'/{unqid()}'\n",
    "        res = path\n",
    "        app.get(route)(lambda: res)\n",
    "        page = TestClient(app).get(route).text\n",
    "        src = f'srcdoc=\"{escape(page)}\"'\n",
    "    if iframe:\n",
    "        return HTML(f'<iframe {src} style=\"width: 100%; height: {height}; border: none;\" onload=\"{scr}\" ' + \"\"\"allow=\"accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking\"></iframe> \"\"\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cf152565",
   "metadata": {},
   "outputs": [],
   "source": [
    "@rt\n",
    "def index():\n",
    "    return Div(\n",
    "        P(A('Click me', hx_get=update, hx_target='#result')),\n",
    "        P(A('No me!', hx_get=update, hx_target='#result')),\n",
    "        Div(id='result'))\n",
    "\n",
    "@rt\n",
    "def update(): return Div(P('Hi!'),P('There!'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "21cb0c40",
   "metadata": {},
   "outputs": [],
   "source": [
    "server.start()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "81669294",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<iframe src=\"http://localhost:8000/\" style=\"width: 100%; height: auto; border: none;\" onload=\"{\n",
       "        let frame = this;\n",
       "        window.addEventListener('message', function(e) {\n",
       "            if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe\n",
       "            if (e.data.height) frame.style.height = (e.data.height+1) + 'px';\n",
       "        }, false);\n",
       "    }\" allow=\"accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking\"></iframe> "
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Run the notebook locally to see the HTMX iframe in action\n",
    "HTMX()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dfc3271d",
   "metadata": {},
   "outputs": [],
   "source": [
    "server.stop()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "850155aa",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def ws_client(app, nm='', host='localhost', port=8000, ws_connect='/ws', frame=True, link=True, **kwargs):\n",
    "    path = f'/{nm}'\n",
    "    c = Main('', cls=\"container\", id=unqid())\n",
    "    @app.get(path)\n",
    "    def f():\n",
    "        return Div(c, id=nm or '_dest', hx_trigger='load',\n",
    "                   hx_ext=\"ws\", ws_connect=ws_connect, **kwargs)\n",
    "    if link: display(HTML(f'<a href=\"http://{host}:{port}{path}\" target=\"_blank\">open in browser</a>'))\n",
    "    if frame: display(HTMX(path, host=host, port=port))\n",
    "    def send(o): asyncio.create_task(app._send(o))\n",
    "    c.on(send)\n",
    "    return c"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "474e14b4",
   "metadata": {},
   "source": [
    "## Export -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d211e8e2",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "import nbdev; nbdev.nbdev_export()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "77112b77",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "python3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
